package com.mh.system.service.impl;

import com.mh.common.constant.Constants;
import com.mh.common.utils.RedisCache;
import com.mh.common.utils.UUIDUtils;
import com.mh.system.domain.LoginUserDetails;
import com.mh.system.service.TokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Service
public class TokenServiceImpl implements TokenService {
    @Autowired
    private RedisCache redisCache;
    //private RedisTemplate redisTemplate;

    //配置文件application.yml中获取值
    @Value("${token.header}")
    private String headerToken; //Token的key: Authorization
    @Value("${token.secret}")
    private String secret; //Token生成密钥: msbhjy
    @Value("${token.expireTime}")
    private int expireTime; //Token有效期: 30

    //1秒: 1000毫秒
    private static final long MILLISECONDS_IN_SECOND = 1000;
    //分钟: 1000毫秒*60 = 1分钟
    private static final long MILLISECONDS_IN_MINUTE = 60 * MILLISECONDS_IN_SECOND;

    //20分钟
    private static final long TWENTY_MINUTE = MILLISECONDS_IN_MINUTE * 20;

    //1. 生成Token
    @Override
    public String createToken(LoginUserDetails loginUser) {

        //通过自定义的工具类设置用户token的唯一标识
        //跟存储在redis的key对应的uuid不同,没关系
        String uuidToken = UUIDUtils.randomUUID();
        loginUser.setTokenUUID(uuidToken);

        //用户信息存入到redis,并给数据设置有效时间
        refreshToken(loginUser);

        HashMap<String, Object> map = new HashMap<>();
        //自定义Map集合,传入Token的唯一标识
        //key: login_user_key , value: uuidToken
        map.put(Constants.LOGIN_USER_KEY,uuidToken);
        String token = Jwts.builder() //通过自定义的工具类创建token
                            .setClaims(map) //自定义map集合
                            .signWith(SignatureAlgorithm.HS256, secret) //设置编码格式 & 密钥
                            .compact(); //关键: 将当前构建的JWT压缩并生成最终的字符串形式
        return token;
    }

    //2. 用户信息存入到redis,并给数据设置有效时间
    @Override
    public void refreshToken(LoginUserDetails loginUser) {
        //给用户设置登录时间
        loginUser.setLoginTime(System.currentTimeMillis());
        //设置过期时间: 登录时间毫秒数+过期时间毫秒数
        //expireTime上面注入的yml的常量30 * MILLISECONDS_IN_MINUTE上面设置的常量1分钟(1000*60) = 30分钟
        loginUser.setExpireTime(loginUser.getLoginTime()+expireTime * MILLISECONDS_IN_MINUTE);

        //根据UUID将loginUser缓存
        //login_tokens:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        String userKey = Constants.LOGIN_TOKEN_KEY + loginUser.getTokenUUID();
        /** 存到redis中,设置参数
         *  参数1: key = 用户唯一标识 前缀+生成的tokenUUID
         *  参数2: value = 用户信息 loginUser(SysUser,tokenUUID,loginTime,expireTime)
         *  参数3: 过期时间 = 上面定义的常量30(来自yml)
         *  参数4: 过期时间的单位 = 小时
         */
        redisCache.setCacheObject(userKey,loginUser,expireTime, TimeUnit.MINUTES);
    }

    //3. 获取用户信息
    @Override
    public LoginUserDetails getLoginUser(HttpServletRequest req) {
        //1. 获取Token(下面自定义的方法)
        String token = getToken(req);
        System.out.println("2.tokenservice获取到token:"+token);

        //如果Token非空,解析
        if(!StringUtils.isEmpty(token)){
            //2. 解析Token(下面自定义的方法)
            //token的有效信息在payload, 解析后用Claims接收, 是一个键值对的集合
            Claims payload = parseToken(token);
            //当初生成Token时,我存进去的键值对,现在我用key值获取value-->用在后在redis的key的一部分
            //生成token时前缀(响应前端的token): map.put(Constants.LOGIN_USER_KEY,uuidToken);
            String valueTokenUUID = (String)payload.get(Constants.LOGIN_USER_KEY);

            System.out.println("3.解析token成功,TokenUUID:"+valueTokenUUID);

            //拼接redis的key值-->当时用户信息存储在redis中时,key值为LOGIN_TOKEN_KEY+tokenUUID
            String redisKey = Constants.LOGIN_TOKEN_KEY + valueTokenUUID;
            LoginUserDetails loginUser = (LoginUserDetails)redisCache.getCacheObject(redisKey);

            System.out.println("4.tokenservice获取到loginuser:"+loginUser);
            return loginUser;
        }
        return null; //token为空,返回null
    }

    //4. 验证Token有效期 & 实现自动刷新缓存
    @Override
    public void verifyToken(LoginUserDetails loginUser) {
        Long expireTime = loginUser.getExpireTime(); //用户过期时间
        long nowTime = System.currentTimeMillis(); //当前时间

        //过期时间-当前时间 少于 20分钟的话, 调用refreshToken重新设定新的过期时间
        if(expireTime - nowTime <= TWENTY_MINUTE){
            refreshToken(loginUser);
        }
    }

    //5. 设置用户身份信息
    @Override
    public void setUser(LoginUserDetails loginUser) {
        //loginUser非空 & TokenUUID也非空的话, 调用refreshToken更新用户信息
        if (!Objects.isNull(loginUser) && loginUser.getTokenUUID() != null) {
            refreshToken(loginUser);
        }
    }

    //6. 删除用户
    @Override
    public void deleteUser(String tokenUUID) {
        //如果传入的Token非空
        if(!StringUtils.isEmpty(tokenUUID)){
            String redisKey = Constants.LOGIN_TOKEN_KEY + tokenUUID;
            redisCache.deleteObject(redisKey);
        }
    }

    //辅助功能1: 从请求头中获取Token
    private String getToken(HttpServletRequest req){
        /**  yml定义headerToken: Authorization
         *  上面常量中已获取到 @Value("${token.headerToken}") = Authorization
         *  this.headerToken就是"Authorization"字符串-->Authorization就是Token的key
         */
        String token = req.getHeader(this.headerToken);

        /** OAuth 2.0 的 Access Token 通常是 Bearer Token
         *  标准格式  Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
         */
        //判断: Token不为空 并且 以"Bearer "开头的话, 把前缀"Bearer "删除, 否则返回Token
        //.startsWith() String的方法-->是否以XX开头
        //.replace(将什么,修改为什么)
        if (!StringUtils.isEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)){
            token = token.replace(Constants.TOKEN_PREFIX,"");
        }
        return token;
    }

    //辅助功能2: 解析Token
    private Claims parseToken(String headerToken){
        /** 用于解析 JWT
         *  .parser(): 创建一个 JWT 解析器实例
         *  .setSigningKey(): 设置密钥(上面常量),必须与生成JWT时使用的密钥一致
         *  .parseClaimsJws(): 解析传入的JWT, 并验证其签名
         *  .getBody(): 获取JWT的负载(Payload),Claims是一个键值对集合,包含用户ID、过期时间等
         */
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(headerToken).getBody();
    }
}
